#!/usr/bin/python

import platform
import sys 
import os
import time
import threading
import subprocess 

system = None
debug = False
profile = False
dependencies = False
verbose = False
release = False
targets = []

global todo, headers, object_deps, file_flags, cflags_glib, cflags_gtk, cflags_gtkgl, libs_glib, libs_gtk, libs_gtkgl, lock, stop
global include_paths
todo, headers, object_deps, file_flags = {}, {}, {}, {}
include_paths = []
lock = threading.Lock()
stop = False



# parse command-line:

for arg in sys.argv[1:]:
  if   arg == '-verbose': verbose = True
  elif arg == '-debug': debug = True
  elif arg == '-deps': dependencies = True
  elif arg == '-profile': profile = True
  elif arg == '-release': release = True
  elif arg.startswith ('-system='): system = arg[8:]
  elif arg[0] == '-':
    print 'unknown command-line option "' + arg + '"'
    sys.stdout.flush()
    sys.exit (1)
  elif arg == 'clean':
    targets = [ 'clean' ]
    break
  else: targets.append(arg)

if system == None: system = platform.system()
exec 'from sysconf.' + system.lower() + ' import *'



# check if we are compiling a separate project:

mrtrix_dir = os.path.dirname (os.path.realpath(sys.argv[0]))
if os.path.abspath(mrtrix_dir) == os.path.abspath(os.getcwd()): mrtrix_dir = None
else:
  if verbose: 
    print 'compiling separate project against "' + mrtrix_dir + '"'
    print ''
  lib_dir = os.path.join (mrtrix_dir, lib_dir)
  misc_dir = os.path.join (mrtrix_dir, misc_dir)
  include_paths = [ 'src' ]


# get version info:

f = open (os.path.join (lib_dir, 'mrtrix.h'), 'r')
for line in f:
  line = line.split()
  if len(line) != 3: continue
  if line[0] == '#define':
    if line[1] == 'MRTRIX_MAJOR_VERSION': major = line[2]
    elif line[1] == 'MRTRIX_MINOR_VERSION': minor = line[2]
    elif line[1] == 'MRTRIX_MICRO_VERSION': micro = line[2]
f.close()

libname += '-' + major + '_' + minor + '_' + micro



# other settings:

ld_flags_lib = [ ld_flags_lib_prefix + libname ]
libname = lib_prefix + libname + lib_suffix
ld_path = [ '-L' + lib_dir ]
include_paths += [ lib_dir, misc_dir ]

if release:
  cpp_flags += cpp_flags_release

if debug:
  cpp_flags = cpp_flags_debug
  ld_flags = ld_flags_debug
  ld_lib_flags = ld_lib_flags_debug

if profile:
  cpp_flags = cpp_flags_profile
  ld_flags = ld_flags_profile
  ld_lib_flags = ld_lib_flags_profile





def pkg (package, target, extras = None):
  cmd = pkgconfig + [ '--' + target, package ]
  if extras: cmd += extras
  process = subprocess.Popen (cmd, stdout=subprocess.PIPE, env=pkgconfig_env)
  if process.wait() != 0:
    print 'WARNING: unable to find', package, 'configuration'
    sys.stdout.flush()
    return None
  return process.stdout.read().split()

cflags_glib = pkg (pkgconfig_glib, 'cflags')
cflags_gtk = pkg (pkgconfig_gtk, 'cflags')
cflags_gtkgl = pkg (pkgconfig_gtk, 'cflags', [ pkgconfig_gl ])
libs_glib = pkg (pkgconfig_glib, 'libs')
libs_gtk = pkg (pkgconfig_gtk, 'libs')
libs_gtkgl = pkg (pkgconfig_gtk, 'libs', [ pkgconfig_gl ])

if verbose:
  print 'Settings:'
  print '  GLib cflags:  ', ' '.join (cflags_glib)
  print '  GTK cflags:   ', ' '.join (cflags_gtk)
  print '  GTK/GL cflags:', ' '.join (cflags_gtkgl)
  print '  GLib libs:    ', ' '.join (libs_glib)
  print '  GTK libs:     ', ' '.join (libs_gtk)
  print '  GTK/GL libs:  ', ' '.join (libs_gtkgl)



###########################################################################
#                           TODO list Entry
###########################################################################

class Entry:
  def __init__ (self, name):
    global todo
    if name in todo.keys(): return
    todo[name] = self

    self.name = name
    self.cmd = []
    self.deps = set()
    self.action = 'NA'
    self.timestamp = mtime (self.name)
    self.dep_timestamp = self.timestamp
    self.currently_being_processed = False

    if is_executable (self.name): self.set_executable()
    elif is_icon (self.name): self.set_icon()
    elif is_object (self.name): self.set_object()
    elif is_library (self.name): self.set_library()

    [ Entry(item) for item in self.deps ]
    dep_timestamp = [ todo[item].dep_timestamp for item in todo.keys() if item in self.deps and not is_library(item) ]
    if len(dep_timestamp): self.dep_timestamp = max(dep_timestamp)
    

  def set_executable (self):
    self.action = 'LB'
    if len(exe_suffix) > 0: cc_file = self.name[:-len(exe_suffix)]
    else: cc_file = self.name
    cc_file = os.path.join (cmd_dir, os.sep.join (cc_file.split(os.sep)[1:])) + cpp_suffix
    self.deps = list_cmd_deps(cc_file)

    try: windres
    except NameError: pass
    else: self.deps.add (icon)

    if file_flags[cc_file] == 1: gtk = libs_glib
    elif file_flags[cc_file] == 2: gtk = libs_gtk
    else: gtk = libs_gtkgl

    self.cmd = fillin (ld, { '$flags$': ld_flags,
      '$path$': ld_path,
      '$obj$': self.deps,
      '$mrtrix$': ld_flags_lib,
      '$gsl$': ld_flags_gsl,
      '$gtk$': gtk,
      '$bin$': [ self.name ] })

    try:
      if ld_use_shell: self.cmd = [ 'sh', '-c', ' '.join(self.cmd) ]
    except NameError: pass

    if not os.path.isdir (bin_dir): os.mkdir (bin_dir)
    self.deps.add (os.path.join (lib_dir, libname))



  def set_object (self):
    self.action = 'CC'
    cc_file = self.name[:-len(obj_suffix)] + cpp_suffix
    self.deps = set([ cc_file ])
    self.deps = self.deps.union (list_headers (cc_file))

    if file_flags[cc_file] == 1: gtk = cflags_glib
    elif file_flags[cc_file] == 2: gtk = cflags_gtk
    else: gtk = cflags_gtkgl

    self.cmd = fillin (cpp, { '$flags$': cpp_flags + cpp_flags_gsl,
      '$path$': [ '-I' + entry for entry in include_paths ],
      '$obj$': [ self.name ],
      '$gtk$': gtk,
      '$src$': [ cc_file ] })


    
  def set_library (self):
    self.action = 'LD'
    for root, dirs, files in os.walk (os.path.split(self.name)[0]):
      for file in files:
        if file[0] == '.': continue
        if file.endswith (cpp_suffix):
          self.deps.add (os.path.join (root, file[:-len(cpp_suffix)] + obj_suffix))

    self.cmd = fillin (ld_lib, { '$flags$': ld_lib_flags,
      '$obj$': [ item for item in self.deps ] + pkg (pkgconfig_glib, 'libs') + ld_flags_gsl,
      '$lib$': [ self.name ] })

    try:
      if ld_use_shell: self.cmd = [ 'sh', '-c', ' '.join(self.cmd) ]
    except NameError: pass


  def set_icon (self):
    self.action = 'WR'
    self.deps = [ icon_dep ]
    self.cmd = windres + [ icon_dep, icon ]


  def display (self):
    print '[' + self.action + '] ' + self.name + ' (' + str(self.timestamp) + ' - ' + str(self.dep_timestamp) + ')',
    if not self.timestamp or self.timestamp < self.dep_timestamp: print '[REBUILD]',
    print ':'
    print '  deps: ', ' '.join(self.deps)
    print '  command: ', ' '.join(self.cmd)
    sys.stdout.flush()






###########################################################################
#                         FUNCTION DEFINITIONS
###########################################################################


def default_targets():
  if not os.path.isdir (cmd_dir): 
    print 'ERROR: no "cmd" folder - unable to determine default targets'
    exit (1)
  for entry in os.listdir (cmd_dir):
    if entry.endswith(cpp_suffix):
      targets.append (os.path.join (bin_dir, entry[:-len(cpp_suffix)] + exe_suffix))
  return targets

def is_executable (target):
  return target.split (os.sep)[0] == bin_dir

def is_library (target):
  return target.endswith (lib_suffix) and target.split(os.sep)[-1].startswith (lib_prefix)

def is_object (target):
  return target.endswith (obj_suffix)

def is_icon (target):
  return target == icon

def mtime (target):
  if not os.path.exists (target): return None
  return os.stat(target).st_mtime

def fillin (template, keyvalue):
  cmd = []
  for item in template:
    if item in keyvalue: cmd += keyvalue[item]
    else: cmd += [ item ]
  return cmd



def execute (message, cmd):
  print message
  sys.stdout.flush()
  if verbose: 
    print ' '.join(cmd)
    sys.stdout.flush()

  try: 
    process = subprocess.Popen (cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if process.wait() != 0:
      print ''
      print 'ERROR:', message
      print ''
      print ' '.join(cmd)
      print ''
      print 'failed with output:'
      print ''
      print process.stderr.read()
      sys.stdout.flush()
      return 1
    out = process.stdout.read()
    if len(out):
      print 'STDOUT:', message
      print out
      sys.stdout.flush()
    out = process.stderr.read()
    if len(out):
      print 'STDERR:', message
      print out
      sys.stdout.flush()

  except OSError:
    print cmd[0] + ': command not found'
    sys.stdout.flush()
    return 1





def list_headers (file):
  global headers, file_flags

  if file not in headers.keys(): 
    headers[file] = set()
    if file == gl: file_flags[file] = 3
    else: file_flags[file] = 1
    if not os.path.exists (file):
      print 'ERROR: cannot find file "' + file + '"'
      sys.stdout.flush()
      exit(1)
    fd = open (file, 'r')
    for line in fd:
      line = line.strip()
      if line.startswith('#include'):
        line = line[8:].strip()
        if line.startswith('<gtk') or line.startswith('<gdk'): file_flags[file] = max (file_flags[file], 2)
        elif line[0] == '"':
          line = line[1:].rstrip('"')
          for path in include_paths:
            if os.path.exists (os.path.join (path, line)):
              line = os.path.join (path, line)
              headers[file].add (line)
              [ headers[file].add(entry) for entry in list_headers(line) ]
              break
          else:
            print 'ERROR: cannot find header file \"' + line + '\" (from file \"' + file + '\")'
            sys.stdout.flush()
            exit(1)
    fd.close()
    flags =  [ file_flags[file] ] + [ file_flags[entry] for entry in headers[file] ]
    if len(flags): file_flags[file] = max (flags)

  return headers[file]






def list_cmd_deps (file_cc):
  global object_deps, file_flags

  if file_cc not in object_deps.keys():
    object_deps[file_cc] = set([ file_cc[:-len(cpp_suffix)] + obj_suffix ])
    for entry in list_headers (file_cc):
      if os.path.abspath(entry).startswith(os.path.abspath(lib_dir)): continue
      entry_cc = entry[:-len(h_suffix)] + cpp_suffix
      if os.path.exists (entry_cc):
        object_deps[file_cc] = object_deps[file_cc].union (list_cmd_deps(entry_cc))
    file_flags[file_cc] = 1
    flags = [ file_flags[entry] for entry in headers[file_cc] ]
    if len(flags): file_flags[file_cc] = max (flags)

  return object_deps[file_cc]





def build_next (id):
  global todo, lock, stop

  try:
    while not stop:
      current = None
      lock.acquire()
      if len(todo):
        for item in todo.keys():
          if todo[item].currently_being_processed: continue
          unsatisfied_deps = set(todo[item].deps).intersection (todo.keys())
          if not len(unsatisfied_deps):
            todo[item].currently_being_processed = True
            current = item
            break
      else: stop = max (stop, 1)
      lock.release()
  
      if stop: return
      if current == None: 
        time.sleep (0.01)
        continue
  
      target = todo[current]
      if execute ('[' + target.action + '] ' + target.name, target.cmd):
        stop = 2
        return

      lock.acquire()
      del todo[current]
      lock.release()

  except:
    stop = 2
    return
    
  stop = max(stop, 1)



def start_html (fid, title, left, up, home, right):
  fid.write ('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html;charset=iso-8859-1">\n')
  fid.write ('<title>MRtrix documentation</title>\n<link rel="stylesheet" href="../stylesheet.css" type="text/css" media=screen>\n</head>\n<body>\n\n')
  fid.write ('<table class=nav>\n<tr>\n<td><a href="' + left + '.html"><img src="../left.png"></a></td>\n')
  fid.write ('<td><a href="' + up + '.html"><img src="../up.png"></a></td>\n')
  fid.write ('<td><a href="' + home + '.html"><img src="../home.png"></a></td>\n')
  fid.write ('<th>' + title + '</th>\n')
  fid.write ('<td><a href="' + right + '.html"><img src="../right.png"></a></td>\n</tr>\n</table>\n')




def gen_command_html_help ():
  binaries = os.listdir (bin_dir)
  binaries.sort()
  description = []

  # generate each program's individual page:
  for n in range (0, len(binaries)):
    print '[DOC] ' + binaries[n]
    sys.stdout.flush()
     
    process = subprocess.Popen ([ binaries[n], '__print_full_usage__' ], stdout=subprocess.PIPE)
    if process.wait() != 0:
      print 'ERROR: unable to execute', binaries[n], ' - aborting'
      sys.stdout.flush()
      return 1
    H = process.stdout.read()
    H = H.splitlines()

    fid = open (os.path.join (doc_dir, 'commands', binaries[n] + '.html'), 'wb')

    if n == 0: prev = 'index'
    else: prev = binaries[n-1]

    if n == len(binaries)-1: next = 'index'
    else: next = binaries[n+1]

    start_html (fid, binaries[n], prev, 'index', '../index', next)
    fid.write ('<h2>Description</h2>\n')

    line = 0
    while not H[line].startswith ('ARGUMENT') and not H[line].startswith('OPTION'):
      if len(description) <= n: description.append (H[line])
      fid.write ('<p>\n' + H[line] + '\n</p>\n')
      line += 1

    arg = []
    opt = []
    while line < len(H)-2:
      if not H[line].startswith ('ARGUMENT') and not H[line].startswith ('OPTION'):
        print 'ERROR: malformed usage for executable "' + binaries[n] + '" - aborting'
        sys.stdout.flush()
        return 1

      if H[line].startswith ('ARGUMENT'):
        S = H[line].split (None, 5)
        A = [ S[1], int(S[2]), int(S[3]), S[4], H[line+1], H[line+2] ]
        if len(opt) > 0: opt[-1].append (A)
        else: arg.append(A)

      elif H[line].startswith ('OPTION'):
        S = H[line].split (None, 4)
        A = [ S[1], int(S[2]), int(S[3]), H[line+1], H[line+2] ]
        opt.append (A)

      line += 3

    fid.write ('<p class=indented><strong>syntax:</strong> &nbsp; &nbsp; ' + binaries[n] + ' [ options ] ')
    for A in arg:
      if A[1] == 0: fid.write ('[ ')
      fid.write (A[0] + ' ')
      if A[2] == 1: fid.write ('[ ' + A[0] + ' ... ')
      if A[1] == 0 or A[2] == 1: fid.write ('] ')
    fid.write ('</p>\n<h2>Arguments</h2>\n<table class=args>\n')


    for A in arg:
      fid.write ('<tr><td><b>' + A[0] + '</b>')
      if A[1] == 0 or A[2] == 1:
        fid.write (' [ ')
        if A[1] == 0: 
          fid.write ('optional')
          if A[2] == 1: fid.write (', ')
        if A[2] == 1: fid.write ('multiples allowed')
        fid.write (' ]')
      fid.write ('</td>\n<td>' + A[5] + '</td></tr>\n')
    fid.write ('</table>\n')

    fid.write ('<h2>Options</h2>\n<table class=args>\n')
    for O in opt:
      fid.write ('<tr><td><b>-' + O[0] + '</b>')
      for A in O[5:]: fid.write ('&nbsp;<i>' + A[0] + '</i>')
      fid.write ('</td>\n<td>' + O[4])
      if len(O) > 5:
        fid.write ('\n<table class=opts>\n')
        for A in O[5:]:
          fid.write ('<tr><td><i>' + A[0] + '</i></td>\n<td>' + A[5] + '</td></tr>\n')
        fid.write ('</table>')
      fid.write ('</td></tr>\n')
    fid.write ('</table>\n')

    fid.write ('</body>\n</html>')

    fid.close()



  fid = open (os.path.join (doc_dir, 'commands', 'index.html'), 'wb')
  start_html (fid, 'list of MRtrix commands', '../faq', '../index', '../index', '../appendix/index')
  fid.write ('<table class=cmdindex width=100%>\n')

  for n in range (0,len(binaries)):
    fid.write ('<tr><td><a href="' + binaries[n] + '.html">' + binaries[n] + '</a></td><td>' + description[n] + '</td></tr>\n')
  fid.write ('</table>\n</body>\n</html>')
  fid.close()




def clean_cmd ():
  files_to_remove = []
  for root, dirs, files in os.walk ('.'):
    for file in files:
      if file[0] == '.': continue
      if file.endswith (obj_suffix) or ( file.startswith (lib_prefix) and file.endswith (lib_suffix) ):
        files_to_remove.append (os.path.join (root, file))
        
  dirs_to_remove = []
  if os.path.isdir (bin_dir):
    for root, dirs, files in os.walk (bin_dir, topdown=False):
      for file in files: files_to_remove.append (os.path.join (root, file))
      for entry in dirs: dirs_to_remove.append (os.path.join (root, entry))
    dirs_to_remove.append (bin_dir)
  
  if os.path.isdir (dev_dir):
    print '[RM] development doc'
    sys.stdout.flush()
    for root, dirs, files in os.walk (dev_dir, topdown=False):
      for entry in files:
        os.remove (os.path.join (root, entry))
    for entry in dirs:
        os.rmdir (os.path.join (root, entry))
    os.rmdir (dev_dir)

  if len(files_to_remove):
    print '[RM] ' + ' '.join(files_to_remove)
    sys.stdout.flush()
    for entry in files_to_remove: os.remove (entry)

  if len(dirs_to_remove):
    print '[RM] ' + ' '.join (dirs_to_remove)
    sys.stdout.flush()
    for entry in dirs_to_remove: os.rmdir (entry)



###########################################################################
#                            START OF PROGRAM
###########################################################################



if 'clean' in targets:
  clean_cmd()
  exit (0)

if doc_dir in targets: 
  exit (gen_command_html_help());
    
if dev_dir in targets: 
  exit (execute ('[DOC] development', [ 'doxygen' ]))
    
if len(targets) == 0: targets = default_targets()

if verbose:
  print ''
  print 'building targets:',
  for entry in targets:
    print entry,
  print ''
  sys.stdout.flush()

if verbose:
  print '' 
  print 'compiling TODO list...'
[ Entry(item) for item in targets ]
for item in todo.keys():
  if todo[item].action == 'NA' or (todo[item].timestamp and todo[item].timestamp >= todo[item].dep_timestamp):
    del todo[item]
if verbose: 
  print 'TODO list contains ' + str(len(todo)) + ' items'
  print ''

#for entry in todo.values(): entry.display()

if dependencies:
  print '======================================='
  print '  Objects'
  print '======================================='
  for entry in object_deps.keys():
    print entry + ' [' + str(file_flags[entry]) + ']:'
    for item in object_deps[entry]: print '  ' + item 

  print '======================================='
  print '  Headers'
  print '======================================='
  for entry in headers.keys():
    print entry + ' [' + str(file_flags[entry]) + ']:'
    for item in headers[entry]: print '  ' + item + ' [' + str(file_flags[item]) + ']'





try: num_processors = os.sysconf('SC_NPROCESSORS_ONLN')
except:
  try: num_processors = int(os.environ['NUMBER_OF_PROCESSORS'])
  except: num_processors = 1
  
if verbose:
  print '' 
  print 'launching ' + str(num_processors) + ' threads'
  print '' 

threads = []
for i in range (1, num_processors):
  t = threading.Thread (target=build_next, args=(i,));
  t.start()
  threads.append (t)

build_next(0)

for t in threads: t.join()

exit (stop > 1)


